package win.start;

/**
 * Created by 张少昆 on 2017/6/12.
 */


import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.charset.CodingErrorAction;
import java.util.Arrays;

import javax.net.ssl.SSLContext;

import org.apache.http.Consts;
import org.apache.http.Header;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.ParseException;
import org.apache.http.client.CookieStore;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.config.AuthSchemes;
import org.apache.http.client.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.config.ConnectionConfig;
import org.apache.http.config.MessageConstraints;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.config.SocketConfig;
import org.apache.http.conn.DnsResolver;
import org.apache.http.conn.HttpConnectionFactory;
import org.apache.http.conn.ManagedHttpClientConnection;
import org.apache.http.conn.routing.HttpRoute;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.DefaultHttpResponseFactory;
import org.apache.http.impl.client.*;
import org.apache.http.impl.conn.DefaultHttpResponseParser;
import org.apache.http.impl.conn.DefaultHttpResponseParserFactory;
import org.apache.http.impl.conn.ManagedHttpClientConnectionFactory;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.impl.conn.SystemDefaultDnsResolver;
import org.apache.http.impl.io.DefaultHttpRequestWriterFactory;
import org.apache.http.io.HttpMessageParser;
import org.apache.http.io.HttpMessageParserFactory;
import org.apache.http.io.HttpMessageWriterFactory;
import org.apache.http.io.SessionInputBuffer;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicLineParser;
import org.apache.http.message.LineParser;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.util.CharArrayBuffer;
import org.apache.http.util.EntityUtils;

/**
 * 示例如何定制和配置HTTP请求的执行及连接管理中最常用的方面。
 */
public class ClientConfiguration {

	public final static void main(String[] args) throws Exception{
		// 使用自定义的message parser / writer 来定制解析HTTP message或将其写入到数据流的方式  -- 没搞懂为啥
		HttpMessageParserFactory<HttpResponse> responseParserFactory = new DefaultHttpResponseParserFactory() {
			@Override
			public HttpMessageParser<HttpResponse> create(SessionInputBuffer buffer, MessageConstraints constraints){
				// 定义个对象
				LineParser lineParser = new BasicLineParser() {
					@Override
					public Header parseHeader(final CharArrayBuffer buffer){
						try{
							return super.parseHeader(buffer);
						} catch(ParseException ex){
							return new BasicHeader(buffer.toString(), null);// 这个重写，就是为了这一句？？？
						}
					}
				};
				return new DefaultHttpResponseParser(buffer, lineParser, DefaultHttpResponseFactory.INSTANCE, constraints) {
					@Override
					protected boolean reject(final CharArrayBuffer line, int count){
						// try to ignore all garbage preceding a status line infinitely
						return false;
					}
				};
			}

		};
		HttpMessageWriterFactory<HttpRequest> requestWriterFactory = new DefaultHttpRequestWriterFactory();

		// Use a custom connection factory to customize the process of
		// initialization of outgoing HTTP connections. Beside standard connection
		// configuration parameters HTTP connection factory can define message
		// parser / writer routines to be employed by individual connections.
		HttpConnectionFactory<HttpRoute, ManagedHttpClientConnection> connFactory = new ManagedHttpClientConnectionFactory(
				requestWriterFactory, responseParserFactory);

		// Client HTTP connection objects when fully initialized can be bound to
		// an arbitrary network socket. The process of network socket initialization,
		// its connection to a remote address and binding to a local one is controlled
		// by a connection socket factory.

		// SSL context for secure connections can be created either based on system or application specific properties.
		SSLContext sslcontext = SSLContexts.createSystemDefault();

		// 注册以支持protocol schemes.
		Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
				.register("http", PlainConnectionSocketFactory.INSTANCE)
				.register("https", new SSLConnectionSocketFactory(sslcontext))
				.build();

		// 使用自定义的 DNS resolver 取代 the system DNS resolution.
		DnsResolver dnsResolver = new SystemDefaultDnsResolver() {
			@Override
			public InetAddress[] resolve(final String host) throws UnknownHostException{
				if(host.equalsIgnoreCase("{myhost}")){  // TODO: {myhost}应该是泛指
					return new InetAddress[]{InetAddress.getByAddress(new byte[]{127, 0, 0, 1})};
				} else{
					return super.resolve(host);
				}
			}
		};

		// 使用自定义的配置来创建一个连接管理器
		PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry, connFactory, dnsResolver);

		// 创建socket configuration
		SocketConfig socketConfig = SocketConfig.custom()
				.setTcpNoDelay(true)
				.build();
		// 配置连接管理器来使用socket configuration，可以用作默认，也可以用于一个特定的host -- 这俩？？？
		connManager.setDefaultSocketConfig(socketConfig);
		connManager.setSocketConfig(new HttpHost("{somehost}", 80), socketConfig);
		// 无反应多久之后验证连接
		connManager.setValidateAfterInactivity(1000);

		// 创建 message constraints
		MessageConstraints messageConstraints = MessageConstraints.custom()
				.setMaxHeaderCount(200)
				.setMaxLineLength(2000)
				.build();
		// 创建 connection configuration
		ConnectionConfig connectionConfig = ConnectionConfig.custom()
				.setMalformedInputAction(CodingErrorAction.IGNORE)
				.setUnmappableInputAction(CodingErrorAction.IGNORE)
				.setCharset(Consts.UTF_8)
				.setMessageConstraints(messageConstraints)
				.build();
		// 配置连接管理器来使用 connection configuration，可以用作默认，也可以用于一个特定的host
		connManager.setDefaultConnectionConfig(connectionConfig);
		connManager.setConnectionConfig(new HttpHost("{somehost}", 80), ConnectionConfig.DEFAULT);

		// 最大并发连接数设置
		connManager.setMaxTotal(100); // 表示连接池最大并发连接数
		connManager.setDefaultMaxPerRoute(10); // 表示单路由的最大并发连接数，假设你的业务系统需要调用A和B这两个外部系统的http接口，那么如果DefaultMaxPerRout=100，那么调用A系统的http接口时，最大并发数就是100
		connManager.setMaxPerRoute(new HttpRoute(new HttpHost("{somehost}", 80)), 20);

		// 如有必要，使用自定义的cookie store
		CookieStore cookieStore = new BasicCookieStore();
		// 如有必要，使用自定义的credentials provider
		CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
		// 创建一个全局的 request configuration
		RequestConfig defaultRequestConfig = RequestConfig.custom()
				.setCookieSpec(CookieSpecs.DEFAULT) //CookieSpecs.IGNORE_COOKIES
				.setExpectContinueEnabled(true) //https://stackoverflow.com/questions/31081061/what-does-setexpectcontinueenabled-mean
				.setTargetPreferredAuthSchemes(Arrays.asList(AuthSchemes.NTLM, AuthSchemes.DIGEST))
				.setProxyPreferredAuthSchemes(Arrays.asList(AuthSchemes.BASIC))
				.build();

		// 创建一个 HttpClient
		CloseableHttpClient httpclient = HttpClients.custom()
				.setConnectionManager(connManager)
				.setDefaultCookieStore(cookieStore)
				.setDefaultCredentialsProvider(credentialsProvider)
				.setProxy(new HttpHost("myproxy", 8080))
				.setDefaultRequestConfig(defaultRequestConfig)
				.setRetryHandler(new DefaultHttpRequestRetryHandler(3,true)) //重试次数3、允许重发已发送的请求
				.build();

		try{
			HttpGet httpget = new HttpGet("{http://httpbin.org/get}");
			// Request configuration 在 request level上优先级比在 client level上高。
			RequestConfig requestConfig = RequestConfig.copy(defaultRequestConfig)
					.setSocketTimeout(5000) //socket读数据超时时间：从服务器获取响应数据的超时时间 或者说 数据传输处理时间
					.setConnectTimeout(5000) //与服务器连接超时时间：httpclient会创建一个异步线程用以创建socket连接，此处设置该socket的连接超时时间
					.setConnectionRequestTimeout(5000) //从连接池中获取连接的超时时间
					.setProxy(new HttpHost("{myotherproxy}", 8080))
					.build();
			httpget.setConfig(requestConfig);

			// 定制执行上下文
			HttpClientContext context = HttpClientContext.create(); //BasicHttpContext，如果不需要设置，传入null也行，默认还是这货
			// 这里设置的上下文attributes，其优先级高于client level设置的attributes 【就是说，二者选一即可？】
			context.setCookieStore(cookieStore);
			context.setCredentialsProvider(credentialsProvider);

			System.out.println("executing request: " + httpget.getURI());
			CloseableHttpResponse response = httpclient.execute(httpget, context); //执行
			// httpclient.execute(httpget); // 如果不需要设置上下文的attributes
			try{
				System.out.println("----------------------------------------");
				System.out.println(response.getStatusLine());
				System.out.println();
				System.out.println(EntityUtils.toString(response.getEntity()));
				System.out.println("----------------------------------------");

				// 一旦请求被执行了，local context就可被用于检查更新后的状态，以及被请求的执行影响的不同的对象

				// 以下都有返回值，略
				// 上一次执行的 request
				context.getRequest();
				// Execution route
				context.getHttpRoute();
				// Target auth state
				context.getTargetAuthState();
				// Proxy auth state
				context.getTargetAuthState();
				// Cookie origin
				context.getCookieOrigin();
				// Cookie spec used
				context.getCookieSpec();
				// User security token
				context.getUserToken();

			} finally{
				response.close();
			}
		} finally{
			httpclient.close();
		}
	}

}

